2026山东大学软件学院创新项目实训博客(六)
【智绘博弈】画风建模系统实现——特征提取与档案生成
项目:智绘博弈 —— 人机对抗绘画猜词与心理解读系统
角色:AI 识别 & 心理解读模块负责人
日期:2026.5.21 - 2026.5.30
一、本周目标
第4篇博客里我写了画风建模的设计方案,第5篇把解析层打稳了。这两周终于到了真正动手实现的阶段:把“给AI加记忆”这件事从方案变成能跑的代码。
任务分三块:
-
设计数据库表 player_style_profiles,存画风档案
-
实现特征提取函数 aggregate_features(),把多局行为数据聚合成量化指标
-
调用 DeepSeek-V3.2 把量化指标转成自然语言档案,注入猜词 prompt
二、画风档案要存什么
在跟队员讨论表结构之前,我先从 AI 模块的角度梳理了”档案需要包含哪些信息”:
-
量化特征——笔速、覆盖率、对称性这些数值,是特征提取函数的直接输出,后续如果要换模型或重新生成档案,原始数据不能丢。我选择用 JSON 序列化存一个字段(
features_json),比拆十几个列灵活,加新维度也不用改表。 -
自然语言档案——DeepSeek 生成的那段话,直接存文本(
profile_text)。猜词的时候取出来注入 prompt 就行,不需要再解析。 -
版本历史——每次更新档案时新建一行而不是覆盖。原因是档案质量不稳定(DeepSeek 偶尔会输出很短或跑题的内容),保留历史版本可以回滚;而且档案的演变本身也有意义,比如在个人主页上展示”你的画风从第3局到第10局的变化”。
具体的建表 SQL 和存取接口是 队员 那边负责的(他的博客里会详细写),我这边主要关注特征提取和档案生成的算法层。
三、特征提取:aggregate_features()
这个函数的工作是把玩家多局的原始行为数据“压缩”成一组代表性指标。
输入是一个列表,每一项是一局的 drawing_behaviors 记录(笔速、覆盖率、撤销次数这些数据);输出是一个字典,包含各指标的多局平均值。
def aggregate_features(behaviors):
n = len(behaviors)
speed_avg = safe_avg('stroke_speed_avg')
speed_var = safe_stdev('stroke_speed_avg')
coverage = safe_avg('canvas_coverage')
symmetry = safe_avg('symmetry_score')
undo_rate = safe_avg('undo_count') + safe_avg('eraser_count')
stroke_count= safe_avg('stroke_count')
turn_density= safe_avg('turn_point_density')
...
tags = _derive_tags(speed_avg, speed_var, coverage, symmetry,
undo_rate, stroke_count, turn_density)
return { 'rounds_analyzed': n, 'tags': tags, ... }
其中 speed_var(笔速方差)是我后来加进去的。最初只有均值,但均值掩盖了一个重要信息:一个玩家可能平均速度是 120px/s,但实际上他时快时慢——开笔很快、收笔很慢,节奏起伏大。另一个玩家平均速度也是 120px/s,但全程匀速。这两种画法对 AI 的影响完全不同,只看均值区分不出来,加了方差之后就有区别了。
派生标签:_derive_tags()
量化指标是数字,但 DeepSeek 更容易理解人话。我写了一个转换函数,把数字映射成描述性标签:
| 条件 | 标签 |
|---|---|
| 笔速 > 200px/s | 快笔型(运笔急促) |
| 笔速 < 50px/s | 慢笔型(运笔缓慢) |
| 撤销+橡皮擦 < 1 次 | 一气呵成(极少修改) |
| 撤销+橡皮擦 > 6 次 | 反复推敲(频繁修改) |
| 画面覆盖率 > 55% | 大胆构图(占满画面) |
| 对称性 > 0.65 | 对称偏好 |
| 笔画数 > 25 | 细节丰富 |
| 拐点密度 < 0.2 | 线条流畅 |
| … | … |
这些标签会拼在 prompt 里,让 DeepSeek 生成档案时有具体的抓手,而不是只看一堆数字。
四、档案生成:调用 DeepSeek-V3.2
特征提取完之后,要把这些数据变成一段"AI能看懂并利用的自然语言"。这一步交给 DeepSeek-V3.2。
为什么不直接把量化指标塞进猜词 prompt?
因为猜词的 prompt 已经在处理"看图猜词"这件事,再塞一堆数字进去,模型要同时理解图像、理解数字、还要猜词,上下文太杂了。提前用 DeepSeek 把数字翻译成自然语言,猜词 prompt 拿到的就是一段流畅的"这个玩家画东西有什么特点"的描述,理解成本低很多。
prompt 设计:
设计 prompt 的时候我想清楚了两点:
-
DeepSeek 要输出的是给另一个 AI 看的描述,不是给人看的,所以要“客观、有识别价值”,不能用“可能”、“也许”这种模糊词
-
格式要固定,方便后面注入猜词 prompt——三块:总体定位 + 关键识别特征 + 画法偏好
最终用的 prompt 核心部分是这样的:
请输出一段100-180字的画风档案,格式为: 1. 总体画风定位(一句话) 2. 关键识别特征(2-3条,便于AI在猜词时参考) 3. 推测画法偏好(例如喜欢先画轮廓再填细节,或反之) 不要使用markdown标题,直接写连贯的中文段落。
实测一个档案的样例输出是这样的:
小明属于快笔简洁型画手,整体风格简练直接。关键识别特征:构图集中在画布右上区域, 笔画数偏少(平均8笔),很少修改,基本一气呵成;线条拐点密度低,多为流畅弧线而非折线。 画法偏好上倾向于先快速勾出主体轮廓,不添加多余细节,抽象程度较高。 AI识别时建议对简笔风格保持更宽松的联想阈值,优先考虑该玩家历史上容易混淆的词对。
这段话注入到猜词 prompt 里之后,AI 猜词的时候就带着”玩家背景”了。设计上预期:对画法比较抽象、以前经常被猜错的玩家提升最大——因为档案里会记住”这个玩家画苹果就是这种风格”,AI 不会再跟没见过一样从零猜。具体的准确率对比数据要等 队员 那边联调跑完完整测试才能出来。
五、降级方案:模板拼接兜底
DeepSeek API 调用可能失败(网络、欠费等)。我加了一个 _template_fallback() 函数,在 LLM 不可用时直接用标签拼一段基础描述:
def _template_fallback(features, nickname):
tags = features.get('tags', [])
lead = '、'.join(tags[:3])
return (
f'{nickname}的画风总体呈现「{lead}」的特征。'
f'平均每局画 {features["avg_stroke_count"]:.0f} 笔、'
f'用时 {features["avg_duration_ms"]/1000:.1f} 秒。'
f'AI识别时可优先关注其惯用构图与笔触节奏。'
)
质量比 DeepSeek 差很多,但够用,保证游戏不中断。这和解析模块的思路一样:LLM 提升质量上限,模板保证下限。
六、接入游戏主流程
写完 style_profiler.py 之后,需要在 app.py 里找合适的时机触发档案刷新。
选择的触发点是每局结束之后,用后台线程异步执行,不阻塞游戏流程:
# 在 _do_end_guessing() 里,结算完分数之后 socketio.start_background_task( _refresh_style_profile_async, drawer_id, room_id )
_refresh_style_profile_async() 内部会判断:如果该玩家局数少于 3 局,直接返回,不生成。只有达到最低局数才真正调 DeepSeek 生成档案。
猜词的时候,ai_recognizer.py 在构建 prompt 前会先查一次档案:
if user_id: style_profile = style_profiler.get_active_profile(user_id) if style_profile: # 档案注入 prompt
整条数据流就打通了:玩家画画 → 行为数据存库 → 每局结束刷新档案 → 下一局猜词时档案注入 prompt。
七、AI辅助开发记录
画风建模系统代码量较大,主要借助 AI(Claude)完成,分几个阶段:
阶段一:数据库表设计
提示词:
player_style_profiles 表要存什么字段?我需要存量化特征(JSON格式)和自然语言档案两部分,另外要支持版本历史,每次更新不覆盖旧记录。帮我设计表结构。
AI 给出了基础表结构,version字段和保留历史版本的思路是 AI 提出的,我接受了这个方案,因为确实比直接覆盖灵活。
阶段二:特征聚合函数
提示词:
帮我写 aggregate_features(),输入是多局 drawing_behaviors 记录的列表,每条记录包含 stroke_speed_avg、canvas_coverage、symmetry_score、undo_count、eraser_count、stroke_count、drawing_duration_ms、turn_point_density、color_count 这些字段。输出是多局的平均值字典,再加一组派生的自然语言标签(比如"快笔型"、"一气呵成"这种)。
AI 给了一版,但只有均值,没有方差。我自己补了 speed_var = safe_stdev('stroke_speed_avg'),理由是均值掩盖了节奏起伏信息。另外派生标签的阈值(比如笔速 > 200 才算“快笔”)我也根据实际测试调了几次,不是 AI 给的初始值。
阶段三:DeepSeek prompt 设计
这部分我和 AI 来回改了好几次。最初 AI 给的 prompt 生成的档案里有"可能"、"也许"这些词,导致档案对猜词 AI 的指导价值很低——模糊的描述不如不给。我加了 system prompt 的约束:
不要出现"可能"、"也许"这类模糊词,给出明确的判断。
第二个问题是输出格式不固定,有时候 AI 自己加了 Markdown 小标题。我在 user prompt 最后加了“不要使用Markdown标题,直接写连贯的中文段落”这句话,才稳定下来。
八、遇到的问题
异步刷新档案时的线程安全
_refresh_style_profile_async() 在后台线程里跑,里面要读数据库。SQLite 默认不支持跨线程共享连接,最开始直接崩了。
解决方法:每次数据库操作都用 get_db_connection() 上下文管理器新建连接,用完即关,不复用连接。这是 models.py 里本来就有的模式,只是异步任务里忘了遵守。
DeepSeek 生成的档案有时太短
temperature 设为 0.4,偶尔 DeepSeek 生成的档案只有40-50字,远低于要求的100字。研究了一下,发现是量化指标里很多字段是 0(玩家只玩了3局,数据样本少),DeepSeek 看到一堆零觉得"没什么好说的",就输出得很简短。
临时解法是在 prompt 里加了一句:"即使部分数据接近0,也请给出基于现有特征的完整分析,不要因数据少而省略段落。"加了之后长度稳定多了。
九、下周计划
-
反向猜词模式下 AI 的 prompt 策略要怎么调整(回放画作而非实时画作,上下文不同)
-
限定画具模式:几何拼图下 AI 识别难度暴增,prompt 需要针对性优化
-
配合 队员 做画风档案注入前后的准确率对比测试
十、本周总结
画风建模从方案变成了跑通的代码。印象最深的是 prompt 设计那一块——同样的数据,换一种表达方式,DeepSeek 生成的档案质量差距很大。最开始以为 prompt 工程是“写几句提示词”的小事,实际上改了七八版才稳定,每一版都有具体的失败原因。
另一个感受是分层设计让调试好很多:特征提取、档案生成、注入猜词,三层各自独立,哪层出问题一眼就看出来。如果全揉在一起,调试会非常痛苦。
更多推荐


所有评论(0)