山东大学软件学院创新实训——个人博客(六)
日期:2026 年 5 月 4 日—— 5 月 17 日
项目:绘画 AI 博弈小游戏 —— 人机对抗绘画猜词与心理解读系统
本周核心任务是:为画风建模系统搭建完整的后端支撑,使得 AI 在每一局对抗中都能更精准地理解玩家的绘画特征,形成"越玩越懂"的学习闭环。
一、数据库设计:player_style_profiles 表
1.1 表结构设计
SQL
CREATE TABLE IF NOT EXISTS player_style_profiles (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
room_id TEXT,
version INTEGER DEFAULT 1,
rounds_analyzed INTEGER DEFAULT 0,
features_json TEXT NOT NULL,
profile_text TEXT NOT NULL,
model_used TEXT DEFAULT '',
generated_at REAL NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (room_id) REFERENCES rooms(id)
);
设计要点分析:
-
version 字段的意义
- 每次刷新档案时 +1,形成版本链
- 不覆盖历史记录,便于追踪玩家画风演变
- 前端可展示 "v1 → v2 → v3" 的画风进化曲线
-
features_json 存储方案
Python
# 聚合的量化特征(喂给 prompt 用) { "rounds_analyzed": 5, "avg_stroke_speed": 145.67, # px/ms "stroke_speed_variance": 32.18, # 速度方差(高方差=节奏多变) "avg_canvas_coverage": 0.48, # 画面占比 "avg_symmetry": 0.72, # 对称性得分 "avg_undo_eraser": 2.4, # 平均修改次数 "avg_stroke_count": 18.5, # 平均笔画数 "avg_duration_ms": 32500, # 平均绘画时长 "avg_turn_density": 0.35, # 线条拐点密度 "avg_color_count": 2.8, # 平均用色数 "tags": ["快笔型", "大胆构图", "细节丰富"] } -
profile_text 的 LLM 生成
- 字段存储由 DeepSeek-V3.2 生成的自然语言档案
- 长度 100-200 字,便于 AI prompt 理解
- 包含构图偏好、线条特征、修改习惯等易于识别的描述
二、数据流设计:从 behaviors 到 profile
2.1 完整流程图解
Code
┌─────────────────────────────────────────────────────────────┐
│ 玩家完成绘画 │
│ Canvas.getBehaviorData() → drawing_behaviors 表 │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 后台任务:_refresh_style_profile_async() │
│ 检查:局数 >= STYLE_PROFILE_MIN_ROUNDS (3)? │
└────────────────────┬────────────────────────────────────────┘
│ YES
▼
┌─────────────────────────────────────────────────────────────┐
│ models.get_user_behaviors(user_id, limit=10) │
│ 取最近 N 局的 drawing_behaviors 原始数据 │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ style_profiler.aggregate_features(behaviors) │
│ │
│ • 计算平均值:速度、覆盖率、对称性等 │
│ • 计算方差:识别节奏多变特征 │
│ • 派生标签:快笔型、细节丰富、一气呵成等 │
│ │
│ 输出:features_dict(含量化指标 + 派生标签) │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ style_profiler.generate_profile_text(features) │
│ │
│ • 构建 prompt(包含派生标签) │
│ • 调用 DeepSeek-V3.2 文本模型 │
│ • 生成自然语言档案(100-200字) │
│ │
│ 返回:profile_text(用于 AI prompt 注入) │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ models.save_style_profile(user_id, features, profile) │
│ │
│ • 取当前最高版本号 v_max │
│ • 插入新行:version = v_max + 1 │
│ • 存入 features_json 和 profile_text │
│ • 记录生成时间和使用的模型 │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ player_style_profiles 表新增一行(版本链) │
│ 下次 AI 猜词时调用 get_active_profile() 获取最新档案 │
└─────────────────────────────────────────────────────────────┘
关键设计决策:
-
为什么不覆盖历史记录?
- 保留版本链便于前端展示"画风演变曲线"
- 研究端可以分析单个玩家的成长轨迹
- 数据库存储成本低(只是文本字段)
-
为什么用异步后台任务?
Python
# app.py 中的调用方式 socketio.start_background_task( _refresh_style_profile_async, drawer_id, room_id )- 不阻塞游戏流程,回合结果立即返回
- 避免 LLM 调用延迟影响用户体验
- 失败自动降级(用模板描述)
-
何时触发生成?
Python
STYLE_PROFILE_MIN_ROUNDS = 3 # 至少3局才生成 STYLE_PROFILE_BEHAVIOR_LIMIT = 10 # 每次取最近10局聚合- 前 3 局积累基础特征
- 第 4 局后每局都更新一次
- 只聚合最近 10 局,权重自然递减
三、特征聚合算法详解
3.1 从原始行为数据到派生标签
在 style_profiler.py 中的 aggregate_features() 和 _derive_tags() 函数核心逻辑:
Python
def aggregate_features(behaviors):
"""
输入:多局 drawing_behaviors 行(list of dict)
输出:画风特征字典(量化指标 + 派生标签)
"""
n = len(behaviors)
# 1. 计算基础统计量
speed_avg = statistics.mean([b.get('stroke_speed_avg', 0) for b in behaviors])
speed_var = statistics.stdev([b.get('stroke_speed_avg', 0) for b in behaviors])
coverage_avg = statistics.mean([b.get('canvas_coverage', 0) for b in behaviors])
symmetry_avg = statistics.mean([b.get('symmetry_score', 0) for b in behaviors])
# 2. 派生标签的阈值设定(基于心理学研究)
tags = []
# 速度标签(笔迹学依据:Pulver, 1931)
if speed_avg > 200:
tags.append('快笔型(运笔急促)') # 高速 = 情绪激活
elif speed_avg < 50:
tags.append('慢笔型(运笔缓慢)') # 低速 = 心理退缩
else:
tags.append('中速运笔')
# 节奏稳定性(速度方差大 = 节奏多变 = 思维跳跃)
if speed_var > 80:
tags.append('节奏多变')
# 构图类型(HTP 理论:Buck, 1948)
if coverage_avg > 0.55:
tags.append('大胆构图(占满画面)') # 占比大 = 外向、自信
elif coverage_avg < 0.2:
tags.append('保守构图(大量留白)') # 占比小 = 内向、谨慎
# 对称性偏好(格式塔心理学:Arnheim, 1974)
if symmetry_avg > 0.65:
tags.append('对称偏好') # 追求秩序感
elif symmetry_avg < 0.3:
tags.append('自由构图') # 不拘一格、创意强
return {
'rounds_analyzed': n,
'avg_stroke_speed': round(speed_avg, 2),
'stroke_speed_variance': round(speed_var, 2),
'avg_canvas_coverage': round(coverage_avg, 2),
'avg_symmetry': round(symmetry_avg, 2),
'tags': tags,
# ... 更多指标
}
派生标签的心理学意义:
| 标签 | 心理学解释 | 阈值依据 |
|---|---|---|
| 快笔型 | 情绪激活、兴奋状态 | 速度 > 200 px/ms |
| 慢笔型 | 情绪低沉、心理退缩 | 速度 < 50 px/ms |
| 节奏多变 | 思维跳跃、注意力分散 | 速度方差 > 80 |
| 大胆构图 | 自信、外向、不拘束 | 覆盖率 > 55% |
| 保守构图 | 谨慎、内向、自我保护 | 覆盖率 < 20% |
| 对称偏好 | 追求秩序感、完美主义 | 对称性 > 0.65 |
| 自由构图 | 创意丰富、随性 | 对称性 < 0.3 |
| 一气呵成 | 自信、果断、不修改 | 撤销次数 < 1 |
| 反复推敲 | 谨慎、完美主义、压力大 | 撤销次数 > 6 |
| 细节丰富 | 观察力强、耐心 | 笔画数 > 25 |
| 简笔风格 | 高效、追求简洁 | 笔画数 < 8 |
3.2 为什么采用多层级特征提取?
Code
原始行为数据(drawing_behaviors 表)
↓
├─ stroke_speed_avg
├─ canvas_coverage
├─ turn_point_density
└─ undo_count ... (15+ 个字段)
▼
中间层:量化指标聚合
├─ avg_stroke_speed(10局均值)
├─ stroke_speed_variance(速度方差)
├─ avg_canvas_coverage
└─ ...
▼
高层:派生标签(转化为人话)
├─ "快笔型"
├─ "大胆构图"
├─ "节奏多变"
└─ ...
▼
最终:LLM prompt 注入
"该玩家快笔型、大胆构图、节奏多变...
平均每局用时32秒、笔触18笔、覆盖率48%...
AI应注意其习惯性起笔位置和色彩搭配"
分层好处:
- 量化指标用于统计分析(后端研究)
- 派生标签便于自然语言理解(LLM 理解)
- 原始数据保留(支持未来算法升级)
四、API 设计与实现
4.1 获取画风档案 API
Python
@app.route('/api/style_profile/<user_id>')
def api_get_style_profile(user_id):
"""获取用户当前最新的画风档案 + 历史版本列表"""
latest = models.get_latest_style_profile(user_id)
if not latest:
return jsonify({
'has_profile': False,
'message': '暂无画风档案,至少完成 3 局对战才会生成。',
'history': [],
})
# 最新版档案
history = models.get_style_profile_history(user_id, limit=10)
return jsonify({
'has_profile': True,
'latest': {
'profile_id': latest['id'],
'version': latest['version'],
'rounds_analyzed': latest['rounds_analyzed'],
'profile_text': latest['profile_text'],
'features': latest.get('features', {}), # 量化指标供前端画图
'model_used': latest['model_used'], # 标识 LLM 版本
'generated_at': latest['generated_at'],
},
'history': history, # 用于展示版本演变
})
返回数据示例:
JSON
{
"has_profile": true,
"latest": {
"profile_id": "abc12345",
"version": 3,
"rounds_analyzed": 8,
"profile_text": "该玩家是典型的快笔型、大胆构图者。笔触速度均值145px/ms,...",
"features": {
"avg_stroke_speed": 145.67,
"avg_canvas_coverage": 0.52,
"avg_symmetry": 0.38,
"tags": ["快笔型", "大胆构图", "自由构图"]
},
"model_used": "deepseek-chat",
"generated_at": 1718625600
},
"history": [
{"version": 3, "rounds_analyzed": 8, "generated_at": 1718625600},
{"version": 2, "rounds_analyzed": 5, "generated_at": 1718539200},
{"version": 1, "rounds_analyzed": 3, "generated_at": 1718452800}
]
}
4.2 内部接口:AI 识别调用
Python
# 在 ai_recognizer.py 中
def recognize_drawing(image_base64, difficulty, category,
word_list=None, user_id=None):
"""
调用 AI 猜词,可选注入玩家画风档案
"""
# 关键:取玩家的最新画风档案
style_profile = ''
if user_id:
try:
from utils import style_profiler
style_profile = style_profiler.get_active_profile(user_id)
# 若有档案,会返回 100-200 字的描述,否则返回空串
except Exception as e:
print(f'[AI] 画风档案加载失败: {e}')
# 构建 prompt(难度相关)
prompt = _build_prompt(difficulty, category, word_list,
style_profile=style_profile)
# 调用多模态 AI(使用档案增强识别精度)
result = _call_zhipu(image_base64, prompt)
return result
prompt 中的档案注入示例:
Python
# 当 style_profile 有值时
prompt += (
f"\n\n【该玩家的画风档案(基于历史绘画行为)】\n"
f"{style_profile}\n"
"请结合该玩家的画风习惯进行识别,但最终判断仍以画面内容为准。"
)
4.3 后台任务:档案刷新
Python
# app.py 中的回合结束处理
def _do_end_guessing(room_id):
"""回合结束,异步刷新画家的画风档案"""
# ... 游戏逻辑 ...
# 触发档案刷新(不阻塞主流程)
try:
drawer_id = round_info.get('drawer_id')
if drawer_id:
socketio.start_background_task(
_refresh_style_profile_async, drawer_id, room_id
)
except Exception as e:
print(f'[StyleProfiler] 触发失败: {e}')
def _refresh_style_profile_async(user_id, room_id):
"""后台异步任务:刷新画风档案"""
try:
from utils import style_profiler
min_rounds = config.STYLE_PROFILE_MIN_ROUNDS # 3
limit = config.STYLE_PROFILE_BEHAVIOR_LIMIT # 10
# 取该玩家最近 10 局的行为数据
behaviors = models.get_user_behaviors(user_id, limit=limit)
if len(behaviors) < min_rounds:
print(f'[StyleProfiler] 玩家 {user_id} 仅 {len(behaviors)} 局,暂不生成')
return
# 转 dict(sqlite Row 需要转换)
behavior_dicts = [dict(b) if not isinstance(b, dict) else b
for b in behaviors]
user = models.get_user(user_id)
nickname = user.get('nickname', '该玩家')
# 核心:构建并保存档案
result = style_profiler.build_and_save_profile(
user_id=user_id,
behaviors=behavior_dicts,
nickname=nickname,
room_id=room_id,
save_to_db=True,
)
print(f'[StyleProfiler] 玩家 {user_id} 档案已刷新: '
f'rounds={result["rounds_analyzed"]}, '
f'model={result["model_used"]}')
except Exception as e:
print(f'[StyleProfiler] 后台刷新异常: {e}')
traceback.print_exc()
五、DeepSeek-V3.2 后端接入
初始化代码(style_profiler.py):
Python
def _call_deepseek(features, nickname):
"""
调用 DeepSeek-V3.2 API 生成画风档案
流程:
1. 初始化客户端(使用 API Key)
2. 构建 system/user 消息
3. 调用 chat.completions.create()
4. 解析响应并返回
"""
try:
from openai import OpenAI
except ImportError:
print('[StyleProfiler] openai SDK 未安装,请 pip install openai')
return _template_fallback(features, nickname)
# ★ 关键:初始化 DeepSeek 客户端
# DeepSeek 提供 OpenAI 兼容接口,URL 和 key 配置即可
client = OpenAI(
api_key=config.DEEPSEEK_API_KEY,
base_url=getattr(config, 'DEEPSEEK_BASE_URL', 'https://api.deepseek.com/v1'),
)
# ★ 构建 prompt
prompt = _build_profile_prompt(features, nickname)
# ★ 调用 API
resp = client.chat.completions.create(
model=getattr(config, 'DEEPSEEK_MODEL', 'deepseek-chat'),
messages=[
{
'role': 'system',
'content': (
'你是一个细致的绘画行为分析师。'
'根据玩家的绘画行为数据,写一段精炼、客观、便于AI识别使用的画风描述。'
'不要出现"可能"、"也许"这类模糊词,给出明确的判断。'
)
},
{
'role': 'user',
'content': prompt
}
],
temperature=0.4, # ★ 控制创意度:0.4 = 较稳定,避免过度创意
max_tokens=400, # ★ 限制输出长度(100-200 字),节省 token
top_p=0.9, # ★ nucleus sampling,增加多样性
)
# ★ 提取结果
text = resp.choices[0].message.content.strip()
print(f'[StyleProfiler] DeepSeek 生成档案 {len(text)} 字')
print(f'[StyleProfiler] 本次调用耗费 token: '
f'input={resp.usage.prompt_tokens}, '
f'output={resp.usage.completion_tokens}')
return text
5.1 Prompt 工程:档案生成策略
Python
def _build_profile_prompt(features, nickname):
"""构建用于 DeepSeek 的 profile 生成 prompt"""
tags_str = '、'.join(features.get('tags', [])) or '暂无明显倾向'
return (
f"玩家昵称:{nickname}\n"
f"已分析局数:{features['rounds_analyzed']}\n\n"
"【量化指标(多局平均)】\n"
f"- 平均笔速:{features['avg_stroke_speed']} px/s\n"
f" (方差 {features['stroke_speed_variance']}) # 方差大=节奏不稳定\n"
f"- 平均画面覆盖率:{features['avg_canvas_coverage']}\n"
f"- 平均对称性得分:{features['avg_symmetry']}\n"
f"- 平均撤销+橡皮擦次数:{features['avg_undo_eraser']}\n"
f"- 平均笔画数:{features['avg_stroke_count']}\n"
f"- 平均绘画时长:{features['avg_duration_ms']} ms\n"
f"- 平均拐点密度:{features['avg_turn_density']}\n"
f"- 平均用色数量:{features['avg_color_count']}\n\n"
f"【派生标签】{tags_str}\n\n"
"请输出一段100-180字的画风档案,格式为:\n"
"1. 总体画风定位(一句话)\n"
"2. 关键识别特征(2-3条,便于AI在猜词时参考)\n"
"3. 推测画法偏好(例如喜欢先画轮廓再填细节,或反之)\n"
"不要使用Markdown标题,直接写连贯的中文段落。"
)
生成的档案示例:
Code
该玩家是典型的快笔型、大胆构图者,运笔速度平均145px/s,
显示较强的情绪激活和行动力。画面覆盖率达52%,明显倾向占满整个
画布,反映出自信和不拘束的性格。虽然对称性评分仅0.38,但这正
体现了其自由、创意驱动的绘画风格——不遵循规则,更注重表达力。
识别建议:该玩家通常先画大轮廓,再快速补充细节,线条曲折度高
(拐点密度0.42),说明思维跳跃。建议AI优先识别其起笔特征和色彩
组合偏好,而非精确轮廓。
5.2 容错机制与降级
Python
def generate_profile_text(features, nickname='该玩家'):
"""
调用 LLM 生成档案,失败时自动降级
"""
try:
if (getattr(config, 'DEEPSEEK_API_KEY', '') and
getattr(config, 'STYLE_MODEL_PROVIDER', '') == 'deepseek'):
# ★ 方案一:LLM 调用
return _call_deepseek(features, nickname)
else:
# ★ 方案二:模板拼接
return _template_fallback(features, nickname)
except Exception as e:
print(f'[StyleProfiler] LLM 调用失败: {e}')
# ★ 方案三:最终降级(完全失败)
return _template_fallback(features, nickname)
def _template_fallback(features, nickname):
"""
无 API Key 或 LLM 失败时的模板拼接方案
保证系统可用性
"""
tags = features.get('tags', [])
if not tags:
return f'{nickname}尚未积累足够画风数据,暂以默认策略识别。'
lead = '、'.join(tags[:3])
detail = '、'.join(tags[3:]) if len(tags) > 3 else ''
parts = [
f'{nickname}的画风总体呈现「{lead}」的特征。',
]
if detail:
parts.append(f'此外还表现出{detail}的倾向。')
parts.append(
f'平均每局画 {features["avg_stroke_count"]:.0f} 笔、'
f'用时 {features["avg_duration_ms"]/1000:.1f} 秒、'
f'画面覆盖率约 {features["avg_canvas_coverage"]:.0%}。'
)
return ''.join(parts)
三层容错设计:
Code
┌─────────────────────────────────┐
│ 尝试方案一:调用 DeepSeek-V3.2 │
│ 成功 ✅ → 返回高质量档案 │
└────────────┬────────────────────┘
│ 失败(API Error, Timeout)
▼
┌─────────────────────────────────┐
│ 尝试方案二:模板拼接 │
│ (无网络也能用) │
│ 成功 ✅ → 返回可用档案 │
└────────────┬────────────────────┘
│ 不适用(无派生标签)
▼
┌─────────────────────────────────┐
│ 方案三:最小化降级 │
│ 返回通用提示 + 基础指标 │
│ ✅ 系统保证可用 │
└─────────────────────────────────┘
六、性能优化与查询设计
6.1 数据库查询优化
Python
def get_user_behaviors(user_id, limit=50):
"""获取用户历史行为数据(带索引优化)"""
with get_db_connection() as conn:
# ★ 利用 INDEX 加速查询
rows = conn.execute(
'''SELECT * FROM drawing_behaviors
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ?''',
(user_id, limit)
).fetchall()
return [dict(r) for r in rows]
def get_latest_style_profile(user_id):
"""获取最新档案(单行查询,O(1))"""
with get_db_connection() as conn:
row = conn.execute(
'''SELECT * FROM player_style_profiles
WHERE user_id = ?
ORDER BY version DESC
LIMIT 1''',
(user_id,)
).fetchone()
if not row:
return None
result = dict(row)
# ★ 解析 JSON 字段
try:
result['features'] = json.loads(result['features_json'])
except (TypeError, json.JSONDecodeError):
result['features'] = {}
return result
数据库索引设计:
SQL
-- 既有索引(从 models.py 初始化)
CREATE INDEX idx_drawing_behaviors_round ON drawing_behaviors(round_id);
-- 新增索引(针对画风系统)
CREATE INDEX idx_drawing_behaviors_user_time ON drawing_behaviors(
user_id, created_at DESC
); -- 加速查询"取该玩家最近 N 局"
CREATE INDEX idx_style_profiles_user_version ON player_style_profiles(
user_id, version DESC
); -- 加速查询"该玩家最新版档案"
6.2 聚合性能分析
Python
def aggregate_features(behaviors):
"""
性能分析:N 局数据聚合耗时
N=10 局时:
- 遍历求平均值:O(N*M) = O(10*15) ≈ 150 ops(毫秒级)
- 计算方差:O(N) ≈ 10 ops(毫秒级)
- 派生标签:O(1)(比对阈值)
- 总耗时:< 5ms(可忽略)
"""
# 1. 计算统计量(Python 的 statistics 模块优化过)
speeds = [b.get('stroke_speed_avg', 0) for b in behaviors]
avg = statistics.mean(speeds) # O(N)
var = statistics.stdev(speeds) # O(N)
# 2. 派生标签(条件判断,O(1))
tags = []
if avg > 200: # 阈值比对
tags.append('快笔型')
return {...}
性能保证:
- 单次聚合 < 10ms
- LLM 调用 (1-3s) 在后台异步运行
- 不影响游戏主流程的响应时间
七、AI辅助开发记录
Prompt1:
我需要设计一个表来存储玩家的"画风档案",要求:
1. 支持版本控制(追踪画风演变,每次更新+1版本号)
2. 存储量化特征(JSON格式,包含笔速、覆盖率、对称性等15+指标)
3. 存储AI生成的自然语言档案(100-200字的文本描述)
4. 记录生成时间和使用的模型信息
请给出:
- 完整的 SQL CREATE TABLE 语句(SQLite)
- 每个字段的设计理由
- 建议的索引策略
- 字段大小和存储成本估算
Prompt2:
我需要在 models.py 中实现以下函数,用于存取 player_style_profiles 表:
1. save_style_profile(user_id, features_dict, profile_text, room_id,
rounds_analyzed, model_used)
- 保存一份新的画风档案
- 自动计算版本号(取当前最高版本+1)
- 不覆盖历史版本,形成版本链
- 返回 profile_id
2. get_latest_style_profile(user_id)
- 获取该用户的最新档案(最高版本号)
- 返回包含解析后 features 字典的完整记录
3. get_style_profile_history(user_id, limit=10)
- 获取该用户的历史版本列表(新到旧)
- 用于前端展示"画风演变"
请生成这三个函数的完整实现,包括:
- SQL 语句
- 错误处理
- 参数校验
- 返回格式统一
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)