【智绘博弈】画风建模系统实现——特征提取与档案生成

项目:智绘博弈 —— 人机对抗绘画猜词与心理解读系统

角色:AI 识别 & 心理解读模块负责人

日期:2026.5.21 - 2026.5.30


一、本周目标

第4篇博客里我写了画风建模的设计方案,第5篇把解析层打稳了。这两周终于到了真正动手实现的阶段:把“给AI加记忆”这件事从方案变成能跑的代码。

任务分三块:

  1. 设计数据库表 player_style_profiles,存画风档案

  2. 实现特征提取函数 aggregate_features(),把多局行为数据聚合成量化指标

  3. 调用 DeepSeek-V3.2 把量化指标转成自然语言档案,注入猜词 prompt

二、画风档案要存什么

在跟队员讨论表结构之前,我先从 AI 模块的角度梳理了”档案需要包含哪些信息”:

  1. 量化特征——笔速、覆盖率、对称性这些数值,是特征提取函数的直接输出,后续如果要换模型或重新生成档案,原始数据不能丢。我选择用 JSON 序列化存一个字段(features_json),比拆十几个列灵活,加新维度也不用改表。

  2. 自然语言档案——DeepSeek 生成的那段话,直接存文本(profile_text)。猜词的时候取出来注入 prompt 就行,不需要再解析。

  3. 版本历史——每次更新档案时新建一行而不是覆盖。原因是档案质量不稳定(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,也请给出基于现有特征的完整分析,不要因数据少而省略段落。"加了之后长度稳定多了。

九、下周计划

  1. 反向猜词模式下 AI 的 prompt 策略要怎么调整(回放画作而非实时画作,上下文不同)

  2. 限定画具模式:几何拼图下 AI 识别难度暴增,prompt 需要针对性优化

  3. 配合 队员 做画风档案注入前后的准确率对比测试

十、本周总结

画风建模从方案变成了跑通的代码。印象最深的是 prompt 设计那一块——同样的数据,换一种表达方式,DeepSeek 生成的档案质量差距很大。最开始以为 prompt 工程是“写几句提示词”的小事,实际上改了七八版才稳定,每一版都有具体的失败原因。

另一个感受是分层设计让调试好很多:特征提取、档案生成、注入猜词,三层各自独立,哪层出问题一眼就看出来。如果全揉在一起,调试会非常痛苦。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐